﻿#region Creditos

/* Autor: Edilberto Sánchez Forero	*/
/* Email: esanchezfo@gmail.com		*/
/* Licencia: LGPL, Software Libre	*/
/* País: Colombia, Bogotá			*/

#endregion

#region Librerias

using System;
using System.Xml;
using System.Xml.Xsl;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.IO;
using System.Text;

using ESF.Object.XmlRepresenter.Rules;
using ESF.General;
using ESF.General.Text;

#endregion


namespace ESF.Object.XmlRepresenter
{
	// TODO: Implementar un watcher para lanzar como eventos de la instancia los cambios que se den en el archivo
	public sealed class XmlObjectSet : IEquatable<XmlObjectSet>
	{	
		#region Constantes
		
		public const string RULES_ELEMENT = "Rules";
		public const string UNIQUE_RULE_ELEMENT = "Unique";
		public const string ONLYONE_RULE_ELEMENT = "OnlyOne";
		public const byte NAME_MAXLENGTH = 10;
		
		#endregion
	
	
		#region Atributos
		
		private string _FileName;
		
		private string _Name;
		
		private DateTime? _LastModificationDateTime;		
		
		private string _Version;
		
		#endregion
		
		
		#region Elementos
		
		private List<XmlObject> _Objects;
		
		#endregion
		
		
		#region Propiedades
		
		public List<XmlObject> Objects { get { return this._Objects; } }
		
		
		public DateTime? LastModificationDateTime { get { return this._LastModificationDateTime; } }
		
		
		public string FileName { get { return this._FileName; } }
		
		
		public string Version 
		{
			get 
			{
				if (string.IsNullOrEmpty (this._Version))
				{
					return "1.0";
				}
				
				return this._Version;
			}
		}
		
		
		public XmlObject this [string pstrObjectName]
		{
			get
			{
				XmlObject objObject;
			
				
				objObject = this.Objects.Find (o => o.Name == pstrObjectName);
				
				if (objObject == null)
				{
					throw new GenericException (GenericException.CRI_UNDEFINED, string.Format ("El objeto con nombre {0} no hace parte del conjunto {1}", pstrObjectName, this._Name));
				}
				
				return objObject;
			}
		}
			
			
		public List<XmlObject> BasicObjects
		{
			get
			{
				return this._Objects.FindAll (o => o.IsBasic);
			}
		}
		
		
		public List<XmlObject> ComplexObjects
		{
			get
			{
				return this._Objects.FindAll (o => !o.IsBasic);
			}
		}
		
		
		public List<XmlObject> NoArrayComplexObjects
		{
			get
			{
				return this._Objects.FindAll (o => !o.IsBasic && !o.IsArray);
			}
		}
		
		
		public List<XmlObject> ArrayComplexObjects
		{
			get
			{
				return this._Objects.FindAll (o => !o.IsBasic && o.IsArray);
			}
		}
			
		
		public List<string> ExternalObjectSetsNamesDependencies
		{
			get
			{
				List<string> strExternalObjectSetNames;
				
				
				strExternalObjectSetNames = new List<string> ();
				
				this.ExternalDefinedObjectMembers.ForEach (
					delegate (XmlMember pobjMember)
					{
						if (!strExternalObjectSetNames.Contains (pobjMember.FromSet))
						{
							strExternalObjectSetNames.Add (pobjMember.FromSet);
						}
					}
				);
				
				return strExternalObjectSetNames;
			}
		}
		
		
		public List<XmlMember> ExternalDefinedObjectMembers 
		{
			get
			{
				List<XmlMember> objMembers;
				
				
				objMembers = new List<XmlMember> ();
				
				this.Objects.ForEach (
					delegate (XmlObject xmlObject)
					{
						objMembers.AddRange (xmlObject.Members.FindAll (m => !string.IsNullOrEmpty (m.FromSet)));
					}
				);
								
				return objMembers;
			}
		} 
			
			
		public string Name { get { return this._Name; } }
			
		#endregion
		
		
		#region Constructor
		
		public XmlObjectSet (string pstrFileName)
		{
			XmlTextReader xmlReader;
			
			XmlMember objCurrentRepresentedMember;
			
			XmlObject objCurrentRepresentedObject;
			
			XmlHelper xmlEx;
			
			
			this._Name = Path.GetFileNameWithoutExtension (pstrFileName);
			
			
			xmlEx = new XmlHelper ();
			xmlEx.ObjectName = "ObjectSet: " + this._Name;
			
			
			this._FileName = pstrFileName;
			
			xmlEx.Validate.FileExists (true, "Archivo del Conjunto de Objetos", this._FileName);
			
			xmlEx.Validate.Filled (true, string.Format ("Nombre del Conjunto de Objetos obtenido del archivo {0}", this._FileName), 
				this._Name, XmlObjectSet.NAME_MAXLENGTH);
			
			xmlReader = null;
			
			#region Carga del archivo
			
			try
			{
				xmlReader = new XmlTextReader (this._FileName);				
			}			
			catch (Exception pexcEx)
			{
				throw new GenericException (GenericException.CRI_FILE_ERROR, string.Format ("El archivo {0} no pudo ser leido", this._FileName), pexcEx);
			}
			
			#endregion
			
			objCurrentRepresentedObject = null;
			
			this._Objects = new List<XmlObject> ();
			
			try
			{
				while (xmlReader.Read ())
				{
					switch (xmlReader.NodeType)
					{
						case XmlNodeType.Element:
							
							#region Represents
							
							// Si es el primer nivel corresponde al elemento de la raiz
							if (xmlReader.Depth == 0)
							{
								xmlEx.EvaluatedNodeReader = xmlReader;
								
								this._LastModificationDateTime = xmlEx.GetAttributeValueDateTime (false, "lastModificationDateTime", "Fecha de la Ultima Modificación");
								
								this._Version = xmlEx.GetAttributeValue (true, "version", "Versión");
							}
							
							#endregion
							
							#region Objetos
							// Si es el siguiente nivel, segundo, donde figuran los representados disponibles
							else if (xmlReader.Depth == 1)
							{
								if (objCurrentRepresentedObject != null)
								{
									if (this.Objects.Contains (objCurrentRepresentedObject))
									{
										throw new GenericException (GenericException.CRI_OBJECT_ALREADYINCOLLECTION, 
											string.Format ("{0} ({1}): {2}", objCurrentRepresentedObject.Name, objCurrentRepresentedObject.Alias,
											"El Nombre y/o el Alias del Objeto ya fueron utilizados en su conjunto, no son unicos"));
									}
									
									this.Objects.Add (objCurrentRepresentedObject);
								}
								
								objCurrentRepresentedObject = new XmlObject (this._Name, ref xmlReader);
							}
							
							#endregion
							
							#region Miembros
							// Si es el tercer nivel corresponde a los miembros
							else if (xmlReader.Depth == 2)
							{
								if (xmlReader.Name != XmlObjectSet.RULES_ELEMENT)
								{
									if (objCurrentRepresentedObject.IsArray)
									{
										throw new GenericException (GenericException.CRI_INVALID_ATTRIBUTE, 
											string.Format ("Un arreglo solo puede contener reglas mas no otros miembros como {0}", xmlReader.Name));
									}
								
									objCurrentRepresentedMember = new XmlMember (ref objCurrentRepresentedObject, ref xmlReader);
									
									if (objCurrentRepresentedObject.Members.Contains (objCurrentRepresentedMember))
									{
										throw new GenericException (GenericException.CRI_INVALID_ATTRIBUTE, 
											string.Format ("{0} ({1}): {2}", objCurrentRepresentedMember.Name, objCurrentRepresentedMember.Alias,
											"El Nombre y/o el Alias del Miembro ya fueron utilizados en su objeto, no son unicos"));
									}
									
									objCurrentRepresentedObject.Members.Add (objCurrentRepresentedMember);
								}
							}
							
							#endregion
							
							#region Reglas
							// Si es el quinto nivel corresponde a los elementos de las reglas predefinidas
							else if (xmlReader.Depth == 3)
							{
								switch (xmlReader.Name)
								{
									case XmlObjectSet.UNIQUE_RULE_ELEMENT:
										objCurrentRepresentedObject.UniqueRules.Add (new UniqueRule (ref objCurrentRepresentedObject, ref xmlReader));
										break;
										
									case XmlObjectSet.ONLYONE_RULE_ELEMENT:
										objCurrentRepresentedObject.OnlyOneRules.Add (new OnlyOneRule (ref objCurrentRepresentedObject, ref xmlReader));
										break;
										
									default:
										throw new GenericException (GenericException.CRI_INVALID_ELEMENT, 
											string.Format ("{0}: El elemento no corresponde a una regla soportada", xmlReader.Name));
								}
							}
							
							#endregion
							
						break;
					}				
				}
				
				this.Objects.Add (objCurrentRepresentedObject);
			
				this.CheckAfterLoad ();
			}
			catch (GenericException gexEx)
			{
				if (xmlReader != null)
				{
					gexEx.LineNumber = xmlReader.LineNumber;
					gexEx.LinePosition = xmlReader.LinePosition;
				}
				
				gexEx.RelatedFilename = this._FileName;
				
				throw gexEx;
			}
			catch (Exception excEx)
			{
				GenericException gexEx;
				
				
				gexEx = new GenericException (GenericException.CRI_READ_ERROR, "No se completo la lectura del archivo", excEx);
				
				if (xmlReader != null)
				{
					gexEx.LineNumber = xmlReader.LineNumber;
					gexEx.LinePosition = xmlReader.LinePosition;
				}
					
				gexEx.RelatedFilename = this._FileName;
				
				throw gexEx;
			}
			finally
			{
				if (xmlReader != null)
				{
					xmlReader.Close ();
				}
			}
		}
		
		
		private void CheckAfterLoad ()
		{
			foreach (XmlObject objObject in this.Objects)
			{
				objObject.CheckAfterLoad (CheckElementExists, CheckMembersElementExistsChecker, CheckMemberElementExists);
			}
		}
		
		
		private bool CheckMembersElementExistsChecker 
		(
			string pstrElementName, List<XmlMember> pstrMemberNames, 
			out List<XmlMember> pobjMembersNotFound
		)
		{
			XmlObject objObject;
			
			
			objObject = this [pstrElementName];
			
			if (objObject == null) { pobjMembersNotFound = null; return false; }
			
			pobjMembersNotFound = new List<XmlMember> ();
			
			foreach (XmlMember objMember in pstrMemberNames)
			{
				pobjMembersNotFound.AddRange (objObject.Members.FindAll (m => m.Name == objMember.Name));
			}
			
			return (pobjMembersNotFound.Count == pstrMemberNames.Count);
		}
		
		
		private bool CheckElementExists (params string [] pstrElementNames)
		{
			int intNotFoundElementsCount;
			
		
			intNotFoundElementsCount = pstrElementNames.Length;
		
			foreach (XmlObject objObject in this.Objects)
			{
				foreach (string strElementName in pstrElementNames)
				{
					if (objObject.Name == strElementName)
					{
						intNotFoundElementsCount --;
					}
				}
			}
			
			return (intNotFoundElementsCount == 0);
		}
		
		
		private bool CheckMemberElementExists (string pstrElementName, string pstrMemberName, out ValueTypes? pobjElementValueType)
		{
			XmlObject objObject;
			
			
			objObject = this [pstrElementName];
			
			if (objObject == null)
			{
				pobjElementValueType = null;
				return false;
			}
			
			if (objObject [pstrMemberName] == null)
			{
				pobjElementValueType = null;
				return false;
			}
			
			pobjElementValueType = objObject [pstrMemberName].Type;
			return true;
		}
		
		#endregion
		
		
		#region Funciones
		
		public void Save ()
		{
			this.SaveAs (this._FileName);
		}
		
		
		public void SaveAs (string pstrFileName)
		{
			File.WriteAllText (pstrFileName, this.ToXmlString ());
		}
		
		
		public XmlDocument ToXml ()
		{
			return this.ToXml (true, true, true, true);
		}
		
		
		public XmlDocument ToXml (bool pblnIncludeGeneralAttributes, bool pblnIncludeMembers, bool pblnIncludeMemberAttributes, bool pblnIncludeRules)
		{
			XmlDocument xmlDoc;
			
			
			xmlDoc = new XmlDocument ();
			xmlDoc.LoadXml (this.ToXmlString (pblnIncludeGeneralAttributes, pblnIncludeMembers, pblnIncludeMemberAttributes, pblnIncludeRules));
			
			return xmlDoc;
		}
		
		
		public string ToXmlString ()
		{
			return this.ToXmlString (true, true, true, true);
		}
		
		
		public string ToXmlString (bool pblnIncludeGeneralAttributes, bool pblnIncludeMembers, bool pblnIncludeMemberAttributes, bool pblnIncludeRules)
		{
			XmlTextWriter xmlWriter;
			
			StringBuilder stbXml;
			
			
			stbXml = new StringBuilder ();
			
			xmlWriter = new XmlTextWriter (new StringWriter (stbXml));
			
			xmlWriter.WriteStartElement ("Objects");
			
			if (pblnIncludeGeneralAttributes)
			{
				if (this._LastModificationDateTime != null)
				{
					xmlWriter.WriteAttributeString ("lastModificationDateTime", this._LastModificationDateTime.Value.ToString ());
				}
			}
			
			foreach (XmlObject objObject in this.Objects)
			{
				objObject.ToXml (ref xmlWriter, pblnIncludeGeneralAttributes, pblnIncludeMembers, pblnIncludeMemberAttributes, pblnIncludeRules);
			}
			
			xmlWriter.WriteEndElement ();			
			xmlWriter.Close ();
			
			return stbXml.ToString ();
		}
		
		
		public bool ExistsObject (string pstrObjectName)
		{
			return this.Objects.Exists (o => o.Name == pstrObjectName);
		}


		public override string ToString ()
		{
			return string.Format ("{0} ({1})", this._Name, this._Objects.Count);
		}
				
		#endregion
	

		#region IEquatable<XmlObjectSet> Members

		public bool Equals (XmlObjectSet pobjObjectSet)
		{
			return (this.Name == pobjObjectSet.Name);
		}

		#endregion
	}
}